iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

Day 5 購物車用 Hash 而不是 String,理由是 HINCRBY 改一個欄位不用讀寫整包。

但同樣一個 Hash、同樣只放一個欄位,有的 key 佔 64 bytes,有的佔 9360 bytes。存的東西一模一樣,差在 Redis 會依資料大小偷偷換底層結構,而且換過去就回不來。

常用指令

今天只有一個指令。

  • OBJECT ENCODING key(這個 key 現在用哪一種底層結構)

https://ithelp.ithome.com.tw/upload/images/20260923/20184209daIuhmS0JK.png

同一個 key 只是把內容從 1 改成 100件,編碼就從 int 變成 embstr 了。

小的時候一種,大了換一種

每個型別都有兩套底層結構:資料少的時候用省空間的那套,多了換成查得快的那套。

型別 小的時候 大了之後
String int(純數字)、embstr(44 字元以內) raw
Hash listpack hashtable
List listpack quicklist
Set intset(全是數字)、listpack hashtable
ZSet listpack skiplist

這些名字不用特別記,只要記得「小的時候一種、大了換一種」就好。

什麼時候換

門檻寫在設定檔裡,CONFIG GET 看得到:

CONFIG GET hash-max-listpack-entries   # 512
CONFIG GET set-max-listpack-entries    # 128
CONFIG GET set-max-intset-entries      # 512
CONFIG GET zset-max-listpack-entries   # 128
CONFIG GET hash-max-listpack-value     # 64

前四個(entries)是「幾筆以內」,最後一個(value)是「單一個值幾字元以內」,超過任何一個就會轉換。

拿 Hash 實測,塞到第 512 個欄位還是 listpack,第 513 個就換掉
(這行在主機的終端機下,不是在 redis-cli 裡面):

for i in $(seq 1 512); do docker exec redis30days redis-cli HSET cart:1 p$i 1 > /dev/null; done
OBJECT ENCODING cart:1  # listpack,512 個欄位還撐得住

HSET cart:1 p513 1
OBJECT ENCODING cart:1  # hashtable,多一個就換掉了

https://ithelp.ithome.com.tw/upload/images/20260923/20184209EPKGHPYDD6.png

值太長也一樣。只放一個欄位,值 64 字元還是 listpack,65 字元直接變 hashtable。

換過去回不來

這是今天真正要記的事。把剛剛那個 Hash 的欄位刪回剩一個:

for i in $(seq 2 513); do docker exec redis30days redis-cli HDEL cart:1 p$i > /dev/null; done
HLEN cart:1             # 1
OBJECT ENCODING cart:1  # hashtable,沒有變回 listpack
MEMORY USAGE cart:1     # 9360

同樣只有一個欄位,另外建一個來對照:

HSET cart:2 p1 1

OBJECT ENCODING cart:2  # listpack
MEMORY USAGE cart:2     # 64

https://ithelp.ithome.com.tw/upload/images/20260923/20184209KW1R6n5Dgg.png

資料一模一樣,量出來差 146 倍。

9360 不全是編碼造成的。hashtable 的桶子是撐到 513 筆的時候配的,沒人碰這個 key 的話它就不會縮,HGET 讀一次之後會掉到 1168。但縮完了還是 1168 比 64,差 18 倍,而且 OBJECT ENCODING 永遠是 hashtable。

要省回來只有一條路:DEL 掉重建。

只有 List 是例外。 撐到 2000 筆變成 quicklist 之後,LTRIM 砍回十筆會自己變回 listpack,LPOP、LREM 也一樣。Hash、Set、ZSet 三個都回不去。

Set 更明顯

Set 全部是數字的時候用 intset,混進一個字串就掉到 listpack:

SADD enc:set 1 2 3 4 5   # intset
SADD enc:set tag         # listpack
SREM enc:set tag         # 還是 listpack

https://ithelp.ithome.com.tw/upload/images/20260923/20184209NZbzAnZLNb.png

那個字串明明已經拿掉了,回不去 intset。跟上面是同一件事。

為什麼要在意

兩種結構可以這樣想:

  • listpack 像一張便條紙,一行一行往下寫,擠得很滿,所以省。缺點是要改中間那行得整張重抄,資料一多就慢。
  • hashtable 像一本有索引的資料夾,每一筆配一個格子,查得快。但格子是事先開好的,只放一筆,空格子照樣佔位置。

小資料用 hashtable,等於為了一張便條紙開一整本資料夾。

一個 key 差一千多 bytes 好像沒什麼,但這種 key 通常是「一個使用者一個」。上面量到的 64 對 1168,一萬個使用者就是 0.6MB 對 11.1MB,而且兩邊 HLEN 都是 1,從外面完全看不出來。

購物車、使用者標籤、Day 8 的延遲關單佇列都是這種「平常很小、偶爾被塞爆」的 key。解法只有兩個:程式裡限制上限(購物車最多幾件),或是定期把這種 key DEL 掉重建。

明天

Day 4 說每個快取 key 都要設 TTL。但設了 TTL 的 key,時間到的那一秒真的被刪掉了嗎?明天繼續看 Redis 怎麼刪過期的 key,還有為什麼 DBSIZE 的數字會騙人/images/emoticon/emoticon33.gif


上一篇
Day 10|Stream
下一篇
Day 12|Key 什麼時候被刪掉
系列文
從購物車到秒殺:30 天 Redis 高併發自學筆記 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言